SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.7 KB · 92 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { eq } from '@/lib/db';5import { db, userBadges } from '@/lib/db';6import { publicProfile } from '@/lib/account/queries';7import { summarizePortfolio } from '@/lib/account/portfolio';8import { BADGE_DEFS } from '@/lib/account/badges';9import { Card, CardHeader, Badge, EmptyState, Delta } from '@/components/ui/primitives';10import { Donut } from '@/components/account/charts';11import { fmtMoney, fmtDate } from '@/lib/format';1213export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }): Promise<Metadata> {14  const { handle } = await params;15  const p = await publicProfile(handle);16  if (!p) return { title: 'Collector not found' };17  const name = p.user.name ?? `@${p.user.handle}`;18  return { title: `${name} · Collector profile`, description: p.user.bio ?? `${name} on RareIndex`, openGraph: { title: `${name} on RareIndex`, description: p.user.bio ?? 'Collector profile', images: [`/u/${handle}/opengraph-image`] } };19}2021export default async function ProfilePage({ params }: { params: Promise<{ handle: string }> }) {22  const { handle } = await params;23  const p = await publicProfile(handle);24  if (!p) notFound();25  const total = summarizePortfolio(p.items);26  const badges = await db().select().from(userBadges).where(eq(userBadges.userId, p.user.id));27  return (28    <div className="mx-auto max-w-4xl">29      <header className="flex flex-col gap-4 py-6 sm:flex-row sm:items-center">30        {p.user.avatarUrl ? (31          // eslint-disable-next-line @next/next/no-img-element32          <img src={p.user.avatarUrl} alt="" className="h-20 w-20 rounded-full border border-border object-cover" />33        ) : (34          <span className="inline-flex h-20 w-20 items-center justify-center rounded-full bg-accent text-2xl font-semibold text-accent-fg">{(p.user.name ?? p.user.handle ?? '?').slice(0, 1).toUpperCase()}</span>35        )}36        <div className="min-w-0 flex-1">37          <h1 className="text-2xl font-semibold tracking-tight">{p.user.name ?? `@${p.user.handle}`}</h1>38          <p className="text-sm text-muted">39            @{p.user.handle} · member since {fmtDate(p.user.createdAt, { month: 'long' })}40          </p>41          {p.user.bio ? <p className="mt-2 max-w-xl text-sm">{p.user.bio}</p> : null}42          {badges.length ? (43            <div className="mt-3 flex flex-wrap gap-1.5">44              {badges.map((b) => (45                <Badge key={b.badge} tone={BADGE_DEFS[b.badge]?.tone ?? 'neutral'}>46                  {BADGE_DEFS[b.badge]?.label ?? b.badge}47                </Badge>48              ))}49            </div>50          ) : null}51        </div>52        <div className="text-right">53          <p className="text-[11px] uppercase tracking-wider text-subtle">Public collection value</p>54          <p className="num text-2xl font-semibold">{total.valuedCount ? fmtMoney(total.valueUsd) : '—'}</p>55          <p className="text-xs text-muted">56            {total.itemCount} item{total.itemCount === 1 ? '' : 's'} across {p.collections.length} public collection{p.collections.length === 1 ? '' : 's'}57          </p>58        </div>59      </header>60      {p.collections.length === 0 ? (61        <Card>62          <EmptyState title="No public collections" description="This collector has not shared any collection yet." />63        </Card>64      ) : (65        <div className="grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">66          <div className="space-y-3">67            {p.collections.map(({ collection: c, summary: s }) => (68              <Link key={c.id} href={`/u/${handle}/${c.publicSlug}`} className="card flex items-center justify-between gap-3 p-4 hover:border-border-strong">69                <div className="min-w-0">70                  <p className="truncate text-sm font-semibold">{c.name}</p>71                  <p className="text-xs text-muted">{c.description ?? `${s.itemCount} items`}</p>72                </div>73                <div className="text-right">74                  <p className="num text-base font-semibold">{s.valuedCount ? fmtMoney(s.valueUsd) : '—'}</p>75                  <Delta value={s.returnPct} className="text-xs" />76                </div>77              </Link>78            ))}79          </div>80          <Card>81            <CardHeader title="Allocation" />82            <div className="p-4">83              <Donut data={total.allocationByFamily.map((b) => ({ label: b.label, value: b.valueUsd }))} size={110} />84            </div>85          </Card>86        </div>87      )}88      <p className="mt-6 text-[11px] text-subtle">Values are RareIndex Valuation estimates with confidence levels; purchase prices are never shown publicly. RareIndex does not authenticate items.</p>89    </div>90  );91}92